package tps12.axwax.server.data;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.json.JSONException;
import org.json.JSONString;
import org.json.JSONStringer;
import org.json.JSONWriter;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;

/**
 * Barred rectangular puzzle object in the data store.
 * 
 * @author Travis
 */
@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class BarredRectangularPuzzle implements JSONString {
	
	private static final Logger log = Logger.getLogger(BarredRectangularPuzzle.class.getName());

	@PrimaryKey
    @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
    private Key key;
    
    public Key getKey() { return key != null ? key : KeyFactory.createKey("Puzzle", 1); }
   
    /**
     * Construct a new puzzle with the given title and dimensions
     * 
     * @param name the puzzle's title
     * @param w the width of the grid
     * @param h the height of the grid
     */
    public BarredRectangularPuzzle(String name, int w, int h) {
    	title = name;
    	width = w;
    	height = h;
    	
    	numbers = new ArrayList<Integer>(w*h);
    	bars = new ArrayList<Short>(w*h);
    	for (int i = 0; i < w*h; i++) {
    		numbers.add(-1);
    		bars.add((short)0);
    	}
    	
    	acrossClues = new ArrayList<String>();
    	acrossNumbers = new ArrayList<Integer>();
    	acrossFormats = new ArrayList<String>();
    	
    	downClues = new ArrayList<String>();
    	downNumbers = new ArrayList<Integer>();
    	downFormats = new ArrayList<String>();
    }

    @Persistent
    private String title;
    
    /**
     * Gets the puzzle title
     */
    public String title() { return title; }
    
    @Persistent
    private int width;
    
    @Persistent
    private int height;
    
    public int getSize() { return width * height; }
    
    @Persistent
    private String instructions;
    
    /**
     * Sets the puzzle's instructions text.
     */
    public void setInstructions(String value) { instructions = value; }
    
    /**
     * Number labels in the grid. Length is equal to width * height, with contents
     * read row by row from the top, and left to right, with -1 indicating a cell
     * be left blank.
     */
    @Persistent
    private List<Integer> numbers;
    
    /**
     * Sets the number shown in the specified grid cell
     * 
     * @param row the cell row
     * @param column the cell column
     * @param number the number to display in the cell
     */
    public void setNumber(int row, int column, int number) {
    	if (numbers == null) {
    		numbers = new ArrayList<Integer>(width * height);
    		for (int i = 0; i < width * height; i++)
   				numbers.add(-1);
    	}
    	numbers.set(row * width + column, number);
    }

    private static final short LEFT_BAR = 0x1;
    private static final short TOP_BAR = 0x2;
    
    /**
     * Bars for each cell, in same order as number list
     */
    @Persistent
    private List<Short> bars;
    
    private void addBar(int row, int column, short bar) {
    	if (bars == null) {
        	bars = new ArrayList<Short>(width * height);
        	for (int i = 0; i < width * height; i++)
        		bars.add((short)0);
    	}
    	bars.set(row * width + column, (short)(bars.get(row * width + column) | bar));
    }
    
    /**
     * Adds a heavy bar to the left of the specified cell
     * 
     * @param row the cell row
     * @param column the cell column
     */
    public void addLeftBar(int row, int column) {
    	addBar(row, column, LEFT_BAR);
    }

    /**
     * Adds a heavy bar above the specified cell
     * 
     * @param row the cell row
     * @param column the cell column
     */
    public void addTopBar(int row, int column) {
    	addBar(row, column, TOP_BAR);
    }

    /**
     * Text of across clues.
     */
    @Persistent
    private List<String> acrossClues;
    
    /**
     * Text of across clue formats.
     */
    @Persistent
    private List<String> acrossFormats;
    
    /**
     * Numbers of across clues.
     */
    @Persistent
    private List<Integer> acrossNumbers;
    
    /**
     * Append a clue to the list of across clues
     * 
     * @param number the clue number
     * @param text the clue text
     * @param format the clue format, excluding parentheses
     */
    public void addAcrossClue(int number, String text, String format) {
    	if (acrossNumbers == null)
    		acrossNumbers = new ArrayList<Integer>();
    	if (acrossClues == null)
    		acrossClues = new ArrayList<String>();
    	if (acrossFormats == null)
    		acrossFormats = new ArrayList<String>();
    	
    	acrossNumbers.add(number);
    	acrossClues.add(text);
    	acrossFormats.add(format);
    }

    /**
     * Append a clue to the list of down clues
     * 
     * @param number the clue number
     * @param text the clue text
     * @param format the clue format, excluding parentheses
     */
    public void addDownClue(int number, String text, String format) {
    	if (downNumbers == null)
    		downNumbers = new ArrayList<Integer>();
    	if (downClues == null)
    		downClues = new ArrayList<String>();
    	if (downFormats == null)
    		downFormats = new ArrayList<String>();
    	
    	downNumbers.add(number);
    	downClues.add(text);
    	downFormats.add(format);
    }

    /**
     * Text of down clues.
     */
    @Persistent
    private List<String> downClues;
    
    /**
     * Text of down clue formats.
     */
    @Persistent
    private List<String> downFormats;
    
    /**
     * Numbers of down clues.
     */
    @Persistent
    private List<Integer> downNumbers;
    
    /**
     * Converts the puzzle to its JSON representation
     */
	@Override
	public String toJSONString() {
		try {
			JSONWriter w = new JSONStringer()
				.object()
					.key("title").value(title)
					.key("instructions").value(instructions != null ? instructions : "")
					.key("grid")
						.array();
			for (int i = 0, n = 0; i < height; i++) {
				w			.array();
				for (int j = 0; j < width; j++, n++) {
					w			.object()
									.key("number").value(numbers.get(n).longValue());
					short bar = bars.get(n);
					w				.key("left").value((bar & LEFT_BAR) == LEFT_BAR)
									.key("top").value((bar & TOP_BAR) == TOP_BAR)
								.endObject();
				}
				w			.endArray();
			}
			w			.endArray()
			
					.key("across")
						.array();
			if (acrossClues != null)
				for (int i = 0; i < acrossClues.size(); i++)
					w		.object()
								.key("number").value(acrossNumbers.get(i).longValue())
								.key("text").value(acrossClues.get(i))
								.key("format").value(acrossFormats.get(i))
							.endObject();
			w			.endArray()
				
					.key("down")
						.array();
			if (downClues != null)
				for (int i = 0; i < downClues.size(); i++)
					w		.object()
								.key("number").value(downNumbers.get(i).longValue())
								.key("text").value(downClues.get(i))
								.key("format").value(downFormats.get(i))
							.endObject();
			w			.endArray()
					.key("letters")
					.value("/" + KeyFactory.keyToString(getKey()) + "/letters/")					
				.endObject();
			return w.toString();
		}
		catch (JSONException e) { 
			log.log(Level.SEVERE, "Creating JSON representation of puzzle failed", e);
			return ""; 
		}
	}
	
	static String parseHtml(Node n, Set<String> tags) {
		if (n.getNodeType() == Node.ELEMENT_NODE) {
			Element e = (Element)n;
			String tag = e.getTagName(), res = "";
			if (tags.contains(tag)) {
				res += "<" + tag + ">";
				res += parseHtml(e, tags);
				res += "</" + tag + ">";
			}
			return res;
		}
		else if (n.getNodeType() == Node.TEXT_NODE)
			return n.getTextContent();
		else
			return "";
	}
	
	static String parseHtml(Element e, Set<String> tags) {
		String res = "";
		
		NodeList children = e.getChildNodes();
		for (int i = 0; i < children.getLength(); i++)
			res += parseHtml(children.item(i), tags);
		
		return res;
	}
	
	/**
	 * Parse XW puzzle data as a barred rectangular puzzle definition
	 * @param xml the xml content
	 * @return the populated puzzle object, or null if parsing failed
	 */
	public static BarredRectangularPuzzle FromXml(Reader xml) {
		DocumentBuilder b;
		try {
			b = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		} catch (ParserConfigurationException e) {
			log.log(Level.SEVERE, "Could not get document builder", e);
			return null;
		}
		
		Element d;
		try {
			d = b.parse(new InputSource(xml)).getDocumentElement();
		} catch (SAXException e) {
			log.log(Level.WARNING, "Parsing XML failed", e);
			return null;
		} catch (IOException e) {
			log.log(Level.SEVERE, "Could not get XML stream", e);
			return null;
		}
		
		// only accept single puzzle element
		NodeList rps;
		try {
			rps = d.getElementsByTagName("rectangular-puzzle");
		}
		catch (Exception e) {
			log.log(Level.WARNING, "Could not get rectangular-puzzle element", e);
			return null;
		}
		if (rps.getLength() != 1) {
			log.log(Level.WARNING, "Expected single rectangular-puzzle element, found " + rps.getLength());
			return null;			
		}
		Element rp = (Element)rps.item(0);

		// get title (may be empty, but element should exist)
		String title;
		try {
			title = ((Element)((Element)rp.getElementsByTagName("metadata").item(0))
				.getElementsByTagName("title").item(0)).getTextContent();
		} catch (Exception e) {
			log.log(Level.WARNING, "Title in metadata not found", e);
			return null;
		}
		
		String instructions = null;
		try {
			instructions = rp.getElementsByTagName("instructions").item(0).getTextContent();
		} catch (Exception e) { }
		
		// get crossword element
		Element cw;
		try {
			cw = (Element)rp.getElementsByTagName("crossword").item(0);
		}
		catch (Exception e) {
			log.log(Level.WARNING, "Didn't find crossword element", e);
			return null;
		}
		
		// get grid
		Element g;
		try {
			g = (Element)cw.getElementsByTagName("grid").item(0);		
		}
		catch (Exception e) {
			log.log(Level.WARNING, "Didn't find grid element", e);
			return null;
		}
	
		// create puzzle object from dimensions
		BarredRectangularPuzzle puzzle;
		try {
			puzzle = new BarredRectangularPuzzle(title,
					Integer.parseInt(g.getAttribute("width")),
					Integer.parseInt(g.getAttribute("height")));
		}
		catch (Exception ex) {
			log.log(Level.WARNING, "Bad or missing dimensions in grid element", ex);
			return null;
		}
		
		// set instructions, if any
		if (instructions != null)
			puzzle.setInstructions(instructions);
		
		NodeList cs = g.getElementsByTagName("cell");
		for (int i = 0; i < cs.getLength(); i++)
			try {
				Element c = (Element)cs.item(i);
				
				// non-default cell types not supported
				if (c.hasAttribute("type")) {
					log.log(Level.WARNING, "Unsupported type attribute in cell " + i);
					return null;
				}
				
				// get location as row and column (0-based) indices
				int row = Integer.parseInt(c.getAttribute("y")) - 1,
					col = Integer.parseInt(c.getAttribute("x")) - 1;
				
				// parse optional features
				if (c.hasAttribute("number"))
					puzzle.setNumber(row, col, Integer.parseInt(c.getAttribute("number")));
				
				if (c.hasAttribute("left-bar") && Boolean.parseBoolean(c.getAttribute("left-bar")))
					puzzle.addLeftBar(row, col);
				
				if (c.hasAttribute("top-bar") && Boolean.parseBoolean(c.getAttribute("top-bar")))
					puzzle.addTopBar(row, col);
				
			} catch (Exception e) {
				log.log(Level.WARNING, "Error parsing cell " + i, e);
			}
			
		// get clues
		NodeList cls;
		try {
			cls = cw.getElementsByTagName("clues");		
		}
		catch (Exception e) {
			log.log(Level.WARNING, "Error getting clues elements", e);
			return null;
		}
	
		// require exactly two sets of clues
		if (cls == null || cls.getLength() != 2) {
			log.log(Level.WARNING, "Expected two sets of clues");
			return null;
		}
		
		Set<String> legalTags = new TreeSet<String>();
		legalTags.add("b");
		legalTags.add("i");
		legalTags.add("u");
		legalTags.add("sub");
		legalTags.add("sup");
		legalTags.add("span");
		
		// first is across, second down
		NodeList a = ((Element)cls.item(0)).getElementsByTagName("clue");
		for (int i = 0; i < a.getLength(); i++)
			try {
				Element c = (Element)a.item(i);
				puzzle.addAcrossClue(Integer.parseInt(c.getAttribute("number")),
						parseHtml(c, legalTags), c.getAttribute("format"));
			} catch (Exception e) {
				log.log(Level.WARNING, "Exception parsing across clue " + i, e);
			}
		a = ((Element)cls.item(1)).getElementsByTagName("clue");
		for (int i = 0; i < a.getLength(); i++)
			try {
				Element c = (Element)a.item(i);
				puzzle.addDownClue(Integer.parseInt(c.getAttribute("number")),
						parseHtml(c, legalTags), c.getAttribute("format"));
			} catch (Exception e) {
				log.log(Level.WARNING, "Exception parsing across clue " + i, e);
			}
			
		return puzzle;
	}
}